// Copyright (c) 2019, the Dart project authors.  Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'dart:async';
import 'dart:io';
import 'dart:typed_data';

import 'package:front_end/src/api_unstable/bazel_worker.dart' as fe;
import 'package:front_end/src/api_prototype/standard_file_system.dart';
import 'package:kernel/target/targets.dart';
import 'package:kernel/kernel.dart';
import 'package:kernel/verifier.dart';
import 'package:vm/modular/target/vm.dart';
import 'package:vm/kernel_front_end.dart';

main() async {
  Uri sdkSummary = sdkRootFile(Platform.executable).resolve('vm_platform.dill');
  if (!await File.fromUri(sdkSummary).exists()) {
    // If we run from the <build-dir>/dart-sdk/bin folder, we need to navigate two
    // levels up.
    sdkSummary = sdkRootFile(
      Platform.executable,
    ).resolve('../../vm_platform.dill');
  }

  // Tests are run in the root directory of the sdk checkout.
  final Uri packagesFile = sdkRootFile('.dart_tool/package_config.json');
  final Uri librariesFile = sdkRootFile('sdk/lib/libraries.json');

  final vmTarget = VmTarget(TargetFlags(supportMirrors: false));

  await withTempDirectory((Uri uri) async {
    final mixinFilename = uri.resolve('mixin.dart');
    final mixinDillFilename = uri.resolve('mixin.dart.dill');
    File.fromUri(mixinFilename).writeAsStringSync(mixinFile);

    await compileToKernel(
      vmTarget,
      librariesFile,
      sdkSummary,
      packagesFile,
      mixinDillFilename,
      <Uri>[mixinFilename],
      <Uri>[],
    );

    final mainFilename = uri.resolve('main.dart');
    final mainDillFilename = uri.resolve('main.dart.dill');
    File.fromUri(mainFilename).writeAsStringSync(mainFile);

    await compileToKernel(
      vmTarget,
      librariesFile,
      sdkSummary,
      packagesFile,
      mainDillFilename,
      <Uri>[mainFilename],
      <Uri>[mixinDillFilename],
    );

    final bytes = concat(
      await File.fromUri(sdkSummary).readAsBytes(),
      concat(
        await File.fromUri(mixinDillFilename).readAsBytes(),
        await File.fromUri(mainDillFilename).readAsBytes(),
      ),
    );
    final component = loadComponentFromBytes(bytes);

    // Verify before running global transformations.
    verifyComponent(
      vmTarget,
      VerificationStage.afterModularTransformations,
      component,
    );

    await runGlobalTransformations(
      vmTarget,
      component,
      ErrorDetector(),
      KernelCompilationArguments(
        useGlobalTypeFlowAnalysis: true,
        enableAsserts: false,
        useProtobufTreeShakerV2: false,
      ),
    );

    // Verify after running global transformations.
    verifyComponent(
      vmTarget,
      VerificationStage.afterGlobalTransformations,
      component,
    );

    // Verify that we can reserialize the component to ensure that all
    // references are contained within the component.
    writeComponentToBytes(
      loadComponentFromBytes(writeComponentToBytes(component)),
    );
  });
}

Future compileToKernel(
  Target target,
  Uri librariesFile,
  Uri sdkSummary,
  Uri packagesFile,
  Uri outputFile,
  List<Uri> sources,
  List<Uri> additionalDills,
) async {
  final state = fe.initializeCompiler(
    null,
    sdkSummary,
    librariesFile,
    packagesFile,
    additionalDills,
    target,
    StandardFileSystem.instance,
    const <String>[],
    const <String, String>{},
  );

  void onDiagnostic(fe.CfeDiagnosticMessage message) {
    message.plainTextFormatted.forEach(print);
  }

  final Component? component = await fe.compileComponent(
    state,
    sources,
    onDiagnostic,
  );
  final Uint8List kernel = fe.serializeComponent(
    component!,
    filter: (library) => sources.contains(library.importUri),
  );
  await File(outputFile.toFilePath()).writeAsBytes(kernel);
}

Future withTempDirectory(Future func(Uri dirUri)) async {
  final dir = await Directory.systemTemp.createTemp('modular-compile-test');
  try {
    await func(dir.uri);
  } finally {
    await dir.delete(recursive: true);
  }
}

Uint8List concat(List<int> a, List<int> b) {
  final bytes = Uint8List(a.length + b.length);
  bytes.setRange(0, a.length, a);
  bytes.setRange(a.length, bytes.length, b);
  return bytes;
}

Uri sdkRootFile(name) => Directory.current.uri.resolveUri(Uri.file(name));

const String mainFile = r'''
import 'mixin.dart';

class R extends A2 {
  void bar() {
    mixinProperty = '';
    mixinProperty.foo();
    mixinMethod('').foo();
    super.mixinProperty= '';
    super.mixinProperty.foo();
    super.mixinMethod('').foo();
  }
}

main() {
  A1();
  final a2 = A2();
  // The mixin deduplication will remove the anonymous mixin application class
  // from `A2 & Mixin` and instead use the one from `A1 & Mixin`.
  a2.mixinProperty= '';
  a2.mixinProperty.foo();
  a2.mixinMethod('').foo();
  R().bar();
  B1();
  B1.named();
  B2();
  final b2 = B2.named();
  // The mixin deduplication will remove the anonymous mixin application class
  // from `B2 & Mixin` and instead use the one from `B1 & Mixin`.
  b2.mixinProperty= '';
  b2.mixinProperty.foo();
  b2.mixinMethod('').foo();
}
''';

const String mixinFile = r'''
class Foo {
  foo() {}
}
class Mixin {
  void set mixinProperty(v) {}
  Foo get mixinProperty => new Foo();
  Foo mixinMethod(v) => new Foo();
}
class A1 extends Object with Mixin { }
class A2 extends Object with Mixin { }
class B {
   B();
   B.named();
}
class B1 extends B with Mixin {
  B1() : super();
  B1.named() : super.named();
}
class B2 extends B with Mixin {
  B2() : super();
  B2.named() : super.named();
}
''';
